package com.agent.vpsserver.common.utils.sign;

import com.agent.vpsserver.common.utils.common.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.Set;

/**
 * 新版本的前面工具
 *
 * @author Godzilla
 * @create 2020-12-08 17:56
 */
@Slf4j
public class SignatureUtils {

    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private static final Random RANDOM = new SecureRandom();

    public static final String FIELD_SIGN = "sign";

    public static final String FIELD_TIMESTAMP = "timestamp";

    public static final String FIELD_NONCESTR = "nonceStr";

    public static final String FIELD_REQUEST_URI_PATH = "REQUEST#URI#PATH";

    public static final String FIELD_REQUEST_BODY = "REQUEST#BODY";

    /**
     * 校验密码并输出日志
     *
     * @param data 请求头和GET请求参数的集合
     * @param key  密钥，登陆后获取，未登录为nonceStr
     * @param url  请求的绝对URL，并去除域名部分得到参与签名的URL，不包含？后面的查询参数
     * @param body 请求报文主体，POST时的body，GET请求时为null，所以不拼接
     * @return
     * @throws Exception
     */
    public static boolean isValidSign(Map<String, String> data, String key, String url, String body) {
        String sign = data.get("sign");
        String strOri = buildingMessage(data, key, url, body);
        String result = MD5(strOri);
        log.info("客户端加密串sign:" + sign);
        log.info("服务端签名前明文:\n" + strOri);
        log.info("服务端签名:" + result);
        return result == null ? false : result.equals(sign);
    }

    /**
     * 判断签名是否正确，必须包含sign、timestamp、nonceStr字段，否则返回false。
     *
     * @param data 请求头和GET请求参数的集合
     * @param key  密钥，登陆后获取，未登录为nonceStr
     * @param url  请求的绝对URL，并去除域名部分得到参与签名的URL，不包含？后面的查询参数
     * @param body 请求报文主体，POST时的body，GET请求时为null，所以不拼接
     * @return
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key, String url, String body) {
        if (CollectionUtils.isEmpty(data) ||
                !data.containsKey(FIELD_SIGN) ||
                !data.containsKey(FIELD_TIMESTAMP) ||
                !data.containsKey(FIELD_NONCESTR)) {
            return false;
        }
        String sign = data.get(FIELD_SIGN);
        String signature = generateSignature(data, key, url, body);
        return signature == null ? false : signature.equals(sign);
    }

    /**
     * 生成签名.获取得到签名串，并对stringSignTemp进行MD5运算，再将得到的字符串所有字符转换为大写，得到sign值signValue。
     *
     * @param headers 请求头和GET请求参数的集合
     * @param key     密钥，登陆后获取，未登录为nonceStr
     * @param url     请求的绝对URL，并去除域名部分得到参与签名的URL，不包含？后面的查询参数
     * @param body    请求报文主体，POST时的body，GET请求时为null，所以不拼接
     * @return 签名
     * @throws Exception
     */
    public static String generateSignature(final Map<String, String> headers, String key, String url, String body) {
        return MD5(buildingMessage(headers, key, url, body));
    }

    /**
     * 获取签名串:
     * URL\n
     * 请求头拼接串\n
     * 请求报文主体\n
     * <p>
     * 其中：URL为获取请求的绝对URL，并去除域名部分得到参与签名的URL，不包含？后面的查询参数。
     * 请求头拼接串为authorize中的json请求体和GET请求中的请求参数总集合M，将集合M内非空参数值的参数按照参数名ASCII码从小到大排序（字典序），使用URL键值对的格式（即key1=value1&key2=value2…）拼接成字符串stringA，
     * 并在最后加上&key=密钥的字符串stringA&key=secretkey
     * 特别注意以下重要规则：
     * ◆ 参数名ASCII码从小到大排序（字典序）；
     * ◆ key为登录后下发的secretkey，如果未登录的情况下，key的值取请求头中的nonce_str;
     * ◆ 如果参数的值为空不参与签名；
     * ◆ 参数名区分大小写；
     * ◆ 传送的sign参数不参与签名，将生成的签名与该sign值作校验。
     * ◆ GET接口参数或者请求头可能增加字段，验证签名时必须支持增加的扩展字段
     * 请求报文主体，POST时的body，GET请求时为null，所以不拼接
     *
     * @param headers 请求头和GET请求参数的集合
     * @param key     密钥，登陆后获取，未登录为nonceStr
     * @param url     请求的绝对URL，并去除域名部分得到参与签名的URL，不包含？后面的查询参数
     * @param body    请求报文主体，POST时的body，GET请求时为null，所以不拼接
     * @return
     */
    public static String buildingMessage(final Map<String, String> headers, String key, String url, String body) {
        Set<String> keySet = headers.keySet();
        String[] keyArray = keySet.toArray(new String[keySet.size()]);
        Arrays.sort(keyArray);
        StringBuilder sb = new StringBuilder();
        sb.append(url).append("\n");
        for (String k : keyArray) {
            if (k.equals(FIELD_SIGN)) {
                continue;
            }
            // 参数值为空，则不参与签名
            String value = headers.get(k);
            if (StringUtils.isNotBlank(value)) {
                sb.append(k).append("=").append(value.trim()).append("&");
            }
        }
        sb.append("key=").append(key).append("\n");
        if (StringUtils.isNotBlank(body)) {
            sb.append(body).append("\n");
        }
        return sb.toString();
    }

    /**
     * 生成 MD5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public static String MD5(String data) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] array = md.digest(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }
            return sb.toString().toUpperCase();
        } catch (Exception e) {
            log.error("MD5签名异常,原文：" + data, e);
        }
        return null;
    }

    /**
     * 生成 HMACSHA256
     *
     * @param data 待处理数据
     * @param key  密钥
     * @return 加密结果
     * @throws Exception
     */
    public static String HMACSHA256(String data, String key) throws Exception {
        Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
        StringBuilder sb = new StringBuilder();
        for (byte item : array) {
            sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString().toUpperCase();
    }

    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }
}
